// 
// SoapTrace.Net - A soap message monitoring utility. 
// 
// Copyright (C) 2004 Matthew Ward
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// Matthew Ward (mrward@users.sourceforge.net)
// 

using System;
using System.Diagnostics;
using System.Text;
using System.Xml;

namespace SoapTrace.Core.Communication
{
	/// <summary>
	/// A soap message sent over Http.
	/// </summary>
	public abstract class SoapMessage : ISoapMessage
	{
		/// <summary>
		/// The current message parsing state.
		/// </summary>
		public enum State : int
		{
			/// <summary>
			/// Waiting for the end of the Http header.
			/// </summary>
			WaitingForEndOfHeader,

			/// <summary>
			/// Waiting for the end of the size line in a chunked
			/// Http message.
			/// </summary>
			WaitingForEndOfChunkSize,

			/// <summary>
			/// Waiting for the last byte of the chunk in chunked
			/// Http message.
			/// </summary>
			WaitingForEndOfChunk,

			/// <summary>
			/// Waiting for the last chunk in Http message.
			/// </summary>
			WaitingForLastChunkCarriageReturnLineFeed,

			/// <summary>
			/// Waiting for the end of the Http message body.
			/// </summary>
			WaitingForEndOfBody,

			/// <summary>
			/// A complete Http message has been received.
			/// </summary>
			MessageReceived
		}

		/// <summary>
		/// The current state of the message parsing.
		/// </summary>
		private SoapMessage.State m_State = SoapMessage.State.WaitingForEndOfHeader; 

		/// <summary>
		/// The message bytes.
		/// </summary>
		private ByteArray m_Bytes = new ByteArray( );

		/// <summary>
		/// The soap message contained in the Http message body.
		/// </summary>
		private string m_Message = String.Empty;
		
		/// <summary>
		/// The message body's length in bytes.
		/// </summary>
		private int m_MessageLength = 0;

		/// <summary>
		/// The date the soap message was received.
		/// </summary>
		private DateTime m_TimeReceived = new DateTime( 2001, 1, 1 );

		/// <summary>
		/// The size of the current chunk we are looking for whilst
		/// parsing.
		/// </summary>
		private int m_ChunkSize;

		/// <summary>
		/// A byte array that holds the bytes containing the line specifying
		/// the size of a chunk.
		/// </summary>
		private ByteArray m_ChunkSizeBytes = new ByteArray( );

		/// <summary>
		/// A byte array that holds the chunk's data.
		/// </summary>
		private ByteArray m_ChunkBytes = new ByteArray( );

		/// <summary>
		/// The number of consecutive carriage return/line feed pairs 
		/// we have received.
		/// </summary>
		private int m_CRLFCount;
		
		/// <summary>
		/// Gets or sets the source of this soap message.
		/// </summary>
		private string m_Source = String.Empty;

		/// <summary>
		/// Creates a new instance of the <see cref="SoapMessage"/> class.
		/// </summary>
		protected SoapMessage( )
		{
		}

		/// <summary>
		/// Gets the soap message string.
		/// </summary>
		public string Message
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_Message;
			}
		}
		
		/// <summary>
		/// The message body's length in bytes.
		/// </summary>
		public int MessageLength
		{
			get
			{
				return m_MessageLength;
			}
		}

		/// <summary>
		/// Gets the Http header.
		/// </summary>
		public string Header
		{
			[ DebuggerStepThrough ]
			get
			{
				return GetSoapHeader( ).Header;
			}
		}

		/// <summary>
		/// Gets or sets the time this soap message received.
		/// </summary>
		public DateTime TimeReceived
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_TimeReceived;
			}

			[ DebuggerStepThrough ]
			set
			{
				m_TimeReceived = value;
			}
		}
		
		/// <summary>
		/// Gets or set the message's source.
		/// </summary>
		/// <remarks>The message source is typically an IP address and
		/// port.</remarks>
		public string Source
		{
			get
			{
				return m_Source;
			}
			
			set
			{
				m_Source = value;
			}
		}

		/// <summary>
		/// Adds and parses a byte received of the Http message.
		/// </summary>
		/// <param name="messageByte">The message byte.</param>
		/// <returns>A status flag indicating whether a complete
		/// Http message has been received.</returns>
		public bool AddByte( byte messageByte )
		{
			bool MessageReceived = false;
			
			if( m_State == SoapMessage.State.MessageReceived )
			{
				Clear( );
				m_State = SoapMessage.State.WaitingForEndOfHeader; 
			}

			if( m_State == SoapMessage.State.WaitingForEndOfHeader )
			{
				SoapHeader Header = GetSoapHeader( );

				if( Header.AddByte( messageByte ) )
				{
					if( Header.IsChunked )
					{
						m_State = SoapMessage.State.WaitingForEndOfChunkSize;
					}
					else if( Header.Header.StartsWith( "GET" ) )
					{
						m_State = SoapMessage.State.MessageReceived;
						MessageReceived = true;
					}
					else
					{
						m_State = SoapMessage.State.WaitingForEndOfBody;
					}
				}
			}
			else if( m_State == SoapMessage.State.WaitingForEndOfBody )
			{
				MessageReceived = AddMessageBodyByte( messageByte );

				if( MessageReceived )
				{
					m_State = SoapMessage.State.MessageReceived;
				}
			}
			else if( m_State == SoapMessage.State.WaitingForEndOfChunkSize )
			{
				bool ChunkSizeReceived = AddChunkSizeByte( messageByte );

				if( ChunkSizeReceived )
				{
					if( m_ChunkSize == 0 )
					{
						m_State = SoapMessage.State.WaitingForLastChunkCarriageReturnLineFeed;
					}
					else
					{
						m_State = SoapMessage.State.WaitingForEndOfChunk;
					}
				}
			}
			else if( m_State == SoapMessage.State.WaitingForEndOfChunk )
			{
				bool ChunkReceived = AddChunkByte( messageByte );
				
				if( ChunkReceived )
				{
					m_State = SoapMessage.State.WaitingForEndOfChunkSize;
				}
			}
			else if( m_State == SoapMessage.State.WaitingForLastChunkCarriageReturnLineFeed )
			{
				if( AddCRLFByte( messageByte ) )
				{
					MessageReceived = true;
					m_State = SoapMessage.State.MessageReceived;

					m_Message = m_Bytes.GetUTF8String( );
				}
			}

			return MessageReceived;
		}

		/// <summary>
		/// Clears the soap message contents.
		/// </summary>
		public void Clear( )
		{
			m_Bytes.Clear( );
			m_ChunkSizeBytes.Clear( );
			m_ChunkBytes.Clear( );
			m_CRLFCount = 0;
			m_MessageLength = 0;
			GetSoapHeader( ).Clear( );
		}

		/// <summary>
		/// Gets the operation of the web service.
		/// </summary>
		public string WebMethod
		{
			[ DebuggerStepThrough ]
			get
			{
				string WebMethod = String.Empty;
				
				if( m_Message.Length > 0 )
				{
					WebMethod = GetWebMethod( );
				}
				
				return WebMethod;
			}
		}

		/// <summary>
		/// Gets the soap header of this message.
		/// </summary>
		/// <returns></returns>
		protected virtual SoapHeader GetSoapHeader( )
		{
			return null;
		}

		/// <summary>
		/// Adds and parses a byte from the Http's message body.
		/// </summary>
		/// <param name="messageByte">A Http message byte.</param>
		/// <returns>A status flag indicating whether the
		/// end of the Http's message body was received.</returns>
		private bool AddMessageBodyByte( byte messageByte )
		{
			bool MessageReceived = false;

			m_Bytes.Add( messageByte );

			if( m_Bytes.Length >= GetSoapHeader( ).ContentLength )
			{
				m_Message = m_Bytes.GetUTF8String( );
				m_MessageLength = m_Bytes.Length;
				MessageReceived = true;
			}

			return MessageReceived;
		}

		/// <summary>
		/// Stores a carriage return/line feed byte so we can tell
		/// whether we are at the end of the Http message or end of a Http line.
		/// </summary>
		/// <param name="messageByte">A carriage return or line feed byte.</param>
		/// <returns>A status flag indicating that we are at the end
		/// of a message (i.e. we have received two CRLFs.</returns>
		private bool AddCRLFByte( byte messageByte )
		{
			bool ReceivedCRLF = false;

			if( m_CRLFCount == 0 )
			{
				if( messageByte == SoapHeader.CR )
				{
					m_CRLFCount = 1;
				}
				else
				{
					Clear( );
					throw new SoapMessageException( "Expecting CR." );
				}
			}
			else
			{
				if( messageByte == SoapHeader.LF )
				{
					ReceivedCRLF = true;
					m_CRLFCount = 0;
				}	
				else
				{
					Clear( );
					throw new SoapMessageException( "Expecting LF." );
				}
			}

			return ReceivedCRLF;
		}

		/// <summary>
		/// Stores a single chunk byte.
		/// </summary>
		/// <param name="messageByte">The chunk byte to store.</param>
		/// <returns>A status flag indicating whether we received
		/// the last byte in a chunk.</returns>
		private bool AddChunkByte( byte messageByte )
		{
			bool EndOfChunk = false;

			m_ChunkBytes.Add( messageByte );

			if( m_ChunkBytes.Length == m_ChunkSize )
			{
				m_Bytes.Add( m_ChunkBytes );
			}
			else if( m_ChunkBytes.Length > m_ChunkSize )
			{
				EndOfChunk = AddCRLFByte( messageByte );

				if( EndOfChunk )
				{
					m_ChunkBytes.Clear( );
				}
			}

			return EndOfChunk;
		}

		/// <summary>
		/// Stores a byte of a Http message body line that contains
		/// a chunk size.
		/// </summary>
		/// <param name="messageByte">A message byte.</param>
		/// <returns>A status flag indicating whether the end of the
		/// chunk size line was received.</returns>
		private bool AddChunkSizeByte( byte messageByte )
		{
			bool EndOfChunkSize = false;

			if( messageByte == SoapHeader.LF )
			{
				EndOfChunkSize = true;

				m_ChunkSize = Convert.ToInt32( m_ChunkSizeBytes.GetUTF8String( ), 16 );
				m_ChunkSizeBytes.Clear( );
			}
			else if( messageByte != SoapHeader.CR )
			{
				m_ChunkSizeBytes.Add( messageByte );
			}

			return EndOfChunkSize;
		}

		/// <summary>
		/// Parses the soap message and retrieves the name of the
		/// web service operation.
		/// </summary>
		/// <returns>The name of the web service operation.</returns>
		private string GetWebMethod( )
		{
			string WebMethod = String.Empty;

			try
			{
				XmlDocument Document = new XmlDocument( );
				Document.LoadXml( m_Message );
	
				XmlNamespaceManager NamespaceManager = 
					new XmlNamespaceManager( Document.NameTable );
	
				NamespaceManager.AddNamespace( "soap", 
					"http://schemas.xmlsoap.org/soap/envelope/" );
	
				XmlElement SoapBody = 
					( XmlElement )Document.SelectSingleNode( "//soap:Body", NamespaceManager );
	
	
				foreach( XmlNode SoapMethodNode in SoapBody.ChildNodes )
				{
					if( SoapMethodNode.NodeType == XmlNodeType.Element )
					{
						WebMethod = SoapMethodNode.LocalName;
						break;
					}
				}
			}
			catch( XmlException )
			{
				// Bad xml, try to get the web method from the soap action.
				
				SoapHeaderField Field = GetSoapHeader( ).Fields.Find( "SoapAction" );
				
				if( Field != null )
				{
					WebMethod = Field.Value.Trim( '"' );
				}
			}
			
			return WebMethod;
		}
	}
}
